package com.mtw.bbs.fileAdmin.util;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mtw.bbs.common.core.exception.BusinessException;
import com.mtw.bbs.fileAdmin.config.QiniuConfiguration;
import com.mtw.bbs.fileAdmin.exception.FileBusException;
import com.mtw.bbs.fileAdmin.mapper.FileShardMapper;
import com.mtw.bbs.fileAdmin.mapper.UploadPartMapper;
import com.mtw.bbs.fileAdmin.pojo.entity.FileShard;
import com.mtw.bbs.fileAdmin.pojo.entity.UploadPart;
import com.qiniu.common.QiniuException;
import com.qiniu.common.Zone;
import com.qiniu.http.Client;
import com.qiniu.http.Response;
import com.qiniu.storage.*;
import com.qiniu.util.Auth;
import com.qiniu.util.StringMap;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;

import static com.mtw.bbs.fileAdmin.constant.FAConstants.REDIS_PART_LOCK_KEY;

@Slf4j
@Component
@RequiredArgsConstructor
public class QiniuTemplate {





    private final RedissonClient redissonClient;
    private final FileShardMapper fileShardMapper;
    private final QiniuConfiguration qiniuConfiguration;
    private final UploadPartMapper uploadPartMapper;
    private static  StringMap putPolicy;


    @PostConstruct
    void intiMap(){
        Map<String, Object> map = new HashMap<>();
        map.put("returnBody", "{\"key\":\"$(key)\",\"hash\":\"$(etag)\",\"bucket\":\"$(bucket)\"}");
        putPolicy = new StringMap(map);
    }




    /**
     * 存储地区配置
     */
    private com.qiniu.storage.Configuration qiNiuConfig() {
        return switch (qiniuConfiguration.getZone()) {
            case "huadong" -> new com.qiniu.storage.Configuration(Zone.huadong());
            case "huabei" -> new com.qiniu.storage.Configuration(Zone.huabei());
            case "huanan" -> new com.qiniu.storage.Configuration(Zone.huanan());
            case "beimei" -> new com.qiniu.storage.Configuration(Zone.beimei());
            default -> throw new FileBusException("存储区域配置错误");
        };
    }

    /**
     * 存储地区配置
     */
    private Region getRegion() {
        return switch (qiniuConfiguration.getZone()) {
            case "huadong" -> Region.huadong();
            case "huabei"  -> Region.huabei();
            case "huanan"  -> Region.huanan();
            case "beimei"  -> Region.beimei();
            default -> throw new FileBusException("存储区域配置错误");
        };
    }

    private String getUpHttp(){
        return switch (qiniuConfiguration.getZone()) {
            case "huadong" -> "https://upload-z0.qiniup.com";
            case "huabei"  -> "https://upload-z1.qiniup.com";
            case "huanan"  -> "https://upload-z2.qiniup.com";
            case "beimei"  -> "https://upload-na0.qiniup.com";
            default -> throw new FileBusException("存储区域配置错误");
        };
    }

    /**
     * 构建一个七牛上传工具实例
     */
    private UploadManager uploadManager() {
        return new UploadManager(qiNiuConfig());
    }

    /**
     * 认证信息实例
     */
    private Auth auth() {
        return Auth.create(qiniuConfiguration.getAccessKey(), qiniuConfiguration.getSecretKey());
    }

    /**
     * 构建七牛空间管理实例
     */
    private BucketManager bucketManager() {
        return new BucketManager(auth(), qiNiuConfig());
    }

    /**
     * 获取上传凭证
     */
    private String getUploadToken() {
        return auth().uploadToken(qiniuConfiguration.getBucket(), null, 3600, putPolicy);
    }




    /**
     * 以文件的形式上传
     *
     * @param file
     * @param fileName:
     */
    public boolean uploadFile(File file, String fileName) throws QiniuException {

        Response response = uploadManager().put(file, fileName, getUploadToken());
        int retry = 0;
        while (response.needRetry() && retry < 3) {
            response = uploadManager().put(file, fileName, getUploadToken());
            retry++;
        }
        return true;
    }


    /**
     * 以流的形式上传
     *
     * @param inputStream
     * @param fileName:
     */
    public boolean uploadFile(InputStream inputStream, String fileName) throws QiniuException {

        Response response =uploadManager().put(inputStream, fileName, getUploadToken(), null, null);
        int retry = 0;
        while (response.needRetry() && retry < 3) {
            response = uploadManager().put(inputStream, fileName, getUploadToken(), null, null);
            retry++;
        }
        return true;
    }



    public String getFileUrl(String fileName) throws QiniuException {
        DownloadUrl url = new DownloadUrl(qiniuConfiguration.getCdnDomain(), false, fileName);
        //有效期
        long expireInSeconds = 86400 ;//24小时，可以自定义链接过期时间
        long deadline = System.currentTimeMillis()/1000 + expireInSeconds;
        return  url.buildURL(auth(), deadline);
    }


    /**
     * 分片上传
     */
    public boolean shardUpload(FileShard fileShard, MultipartFile file) {


        /**
         * 一个文件被分成多个 part，上传所有的 part，然后在七牛云根据 part 信息合成文件
         * |----------------------------- file -----------------------------|
         * |------ part ------|------ part ------|------ part ------|...
         * |----- etag01 -----|----- etag02 -----|----- etag03 -----|...
         * allBlockCtx = [{"partNumber":1, "etag", etag01}, {"partNumber":2, "etag", etag02}, {"partNumber":3, "etag", etag03}, ...]
         *
         * 上传过程：
         * 1. 调用 ApiUploadV2InitUpload api 创建一个 upload 任务，获取 uploadId
         * 2. 重复调用 ApiUploadV2UploadPart api 直到文件所有的 part 均上传完毕, part 的大小可以不相同
         * 3. 调用 ApiUploadV2CompleteUpload api 组装 api
         * 4. ApiUploadV2InitUpload、ApiUploadV2UploadPart、ApiUploadV2CompleteUpload 等分片 V2 API的 key 需要统一（要么有设置且相同，要么均不设置）
         *
         * 注：
         * 1. partNumber 范围是 1 ~ 10000
         * 2. 除最后一个 Part 外，单个 Part 大小范围 1 MB ~ 1 GB
         * 3. 如果你用同一个 PartNumber 上传了新的数据，那么服务端已有的这个号码的 Part 数据将被覆盖
         */

        // 前端传入的分片索引是从0开始，七牛云的分片是从1开始
        fileShard.setIndex(fileShard.getIndex()+1);

        Configuration cfg = new Configuration(getRegion());
        cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;
        cfg.resumableUploadMaxConcurrentTaskCount = 2;
        Client client = new Client(cfg);
        ApiUploadV2InitUpload initUploadApi = new ApiUploadV2InitUpload(client);

        String upHttp = getUpHttp();
        String uploadToken = getUploadToken();

        // 查看是否存在uploadId
        FileShard selected = fileShardMapper.selectById(fileShard.getId());
        if (selected == null || StringUtils.isBlank(selected.getRequestId())) {
            // 加锁
            RLock lock = redissonClient.getLock(REDIS_PART_LOCK_KEY+fileShard.getMd5());
            lock.lock();
            FileShard selected2 = fileShardMapper.selectById(fileShard.getId());
            try {
                // 再检查一次
                if (StringUtils.isBlank(selected2.getRequestId())){
                    ApiUploadV2InitUpload.Request initUploadRequest = new ApiUploadV2InitUpload.Request(upHttp, uploadToken)
                            .setKey(fileShard.getPath());
                    ApiUploadV2InitUpload.Response initUploadResponse = initUploadApi.request(initUploadRequest);
                    String uploadId = initUploadResponse.getUploadId();
                    fileShard.setRequestId(uploadId);
                    fileShardMapper.updateById(fileShard);
                    log.info("获取上传uploadId：{}", uploadId);
                }else {
                    fileShard.setRequestId(selected2.getRequestId());
                }
            }catch (QiniuException e) {
                log.error("获取上传uploadId失败，文件名称:{}",fileShard.getOriginName());
            }finally {
                lock.unlock();
            }
        }else {
            fileShard.setRequestId(selected.getRequestId());
        }


        // 开始上传
        try {
            ApiUploadV2UploadPart.Request uploadPartRequest = new ApiUploadV2UploadPart.Request(upHttp,uploadToken, fileShard.getRequestId(), fileShard.getIndex())
                    .setKey(fileShard.getPath())
                    .setUploadData(file.getBytes(), 0, (int) file.getSize(), null);

            ApiUploadV2UploadPart uploadPartApi = new ApiUploadV2UploadPart(client);
            ApiUploadV2UploadPart.Response uploadPartResponse = uploadPartApi.request(uploadPartRequest);
            String etag = uploadPartResponse.getEtag();

            RLock partLock = redissonClient.getLock(REDIS_PART_LOCK_KEY+fileShard.getMd5()+"_"+fileShard.getIndex());
            partLock.lock();
            try {
                LambdaQueryWrapper<UploadPart> lqw = new LambdaQueryWrapper<>();
                lqw.eq(UploadPart::getShareId,fileShard.getId());
                lqw.eq(UploadPart::getPartNumber,fileShard.getIndex());
                UploadPart uploadPart = uploadPartMapper.selectOne(lqw);
                if (uploadPart != null){
                    uploadPart.setEtag(etag);
                    uploadPart.setPartMd5(uploadPartResponse.getMd5());
                    uploadPartMapper.updateById(uploadPart);
                }else {
                    UploadPart part = new UploadPart();
                    part.setShareId(fileShard.getId());
                    part.setEtag(etag);
                    part.setPartNumber(fileShard.getIndex());
                    part.setPartMd5(uploadPartResponse.getMd5());
                    uploadPartMapper.insert(part);
                }
            }finally {
                partLock.unlock();
            }
            log.info("文件{}分片{}上传完成，requestId：{}",fileShard.getName(),fileShard.getIndex(),fileShard.getId());
        } catch (IOException e) {
            log.error("文件{}分片{}上传失败，requestId：{}，原因",fileShard.getName(),fileShard.getIndex(),fileShard.getId(),e);
        }
        return true;

    }


    /**
     * 合并文件
     */
    public boolean mergeFile(FileShard fileShard) {

        Configuration cfg = new Configuration(getRegion());
        cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;
        cfg.resumableUploadMaxConcurrentTaskCount = 2;
        Client client = new Client(cfg);
        String upHttp = getUpHttp();
        String uploadToken = getUploadToken();

        List<UploadPart> uploadParts = uploadPartMapper.selectList(new LambdaQueryWrapper<UploadPart>()
                                                                                            .eq(UploadPart::getShareId, fileShard.getId()))
                                                        .stream().sorted(Comparator.comparing(UploadPart::getPartNumber)).toList();

        List< Map<String, Object>> partInfoList = new ArrayList<>();
        uploadParts.forEach(x->{
            Map<String, Object> partInfo = new HashMap<>();
            partInfo.put(ApiUploadV2CompleteUpload.Request.PART_NUMBER, x.getPartNumber());
            partInfo.put(ApiUploadV2CompleteUpload.Request.PART_ETG, x.getEtag());
            partInfoList.add(partInfo);
        });

        // 3. 组装文件
        Map<String, Object> customParam = new HashMap<>(4);
        customParam.put("x:foo", "foo-Value");
        ApiUploadV2CompleteUpload completeUploadApi = new ApiUploadV2CompleteUpload(client);
        ApiUploadV2CompleteUpload.Request completeUploadRequest = new ApiUploadV2CompleteUpload.Request(upHttp, uploadToken, fileShard.getRequestId(), partInfoList)
                .setKey(fileShard.getPath())
                .setFileName(fileShard.getOriginName())
                .setCustomParam(customParam);
        try {
            ApiUploadV2CompleteUpload.Response completeUploadResponse = completeUploadApi.request(completeUploadRequest);
            log.info("合并文件{}，结果{}",fileShard.getName(),completeUploadResponse.getResponse());

        } catch (QiniuException e) {
            log.error("合并文件{}出错",fileShard.getName(),e);
            throw new BusinessException(e.getMessage(),e);
        }
        return true;
    }





    /**
     * 删除文件
     *
     * @param key:
     * @return: java.lang.String
     */
    public boolean delete(String key) throws QiniuException {
        Response response = bucketManager().delete(qiniuConfiguration.getBucket(), key);
        int retry = 0;
        while (response.needRetry() && retry++ < 3) {
            response = bucketManager().delete(qiniuConfiguration.getBucket(), key);
        }
        return response.statusCode == 200;
    }








}
